package websocket

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"math/rand"
	"net/http"
	"peilian-api/app/global/common"
	"peilian-api/app/global/databases/tables"
	"peilian-api/app/global/log"
	"peilian-api/app/global/variable"
	"peilian-api/app/services/career_service"
	"peilian-api/app/services/online_service"
	"peilian-api/app/services/report_service"
	"peilian-api/utils/sdk"
	"peilian-api/utils/tools"
	"runtime"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/go-redis/redis"
	"github.com/gorilla/websocket"
)

// websocket 服务器：定义一个 websocket 服务器和客户端连接。

const (
	ConvBorderNum = 4 // 更换问题的边界，前 n 个问题是通用问题，后面问题是职业问题
	SocialCount   = 6 // 社招面试总题数, 通用岗位3道，岗位面试题3道
	PMCount       = 7 // 产品岗保留逻辑面试题，所以一共面7道题
	CivilCount    = 3 // 公务员 或者 通用职业 面试总题数

	MainClassify    = 1 // 主流程练习
	SpecialClassify = 2 // 专项练习

)

type Business struct {
	Session         *tables.Session
	SpecialRecord   *tables.SpecialRecord
	CurrentQuestion *tables.Question
	Criteria        map[int][]string //一共有几个维度：有几个维度就问几个问题 key 是 职业id  value 是string数组
	CurrentTagOld   int              // 当前的问题，前端传入的当前问题
	CurrentTag      int              // 当前是第几个问题,返回给前端的当前问题值
	CurrentRealQues *tables.RealQues
	SpeakTime       int   // 说话时间
	PointStruct     []int // 要点评分
	CurrentId       uint  // 当前问题的id
	CombineText     string
	Classify        uint     // 类型  1: 面试主流程 2：专项练习
	QuestionOrigin  []string // 面试来源
}

type Client struct {
	Id          string
	Socket      *websocket.Conn
	Message     chan []byte
	AudioCh     chan []byte
	TextCh      chan string
	AudioBuffer *bytes.Buffer
	Ctx         *gin.Context
	Bus         Business

	partCacheLabels []interface{}
	Logger          *log.MyLogger
	INextQuestion
}

type VideoInfo struct {
	Video          string
	PcVideo        string
	Text           string
	QuestionOrigin string
}

func (c *Client) Close() {
	//客户端关闭
	if err := c.Socket.Close(); err != nil {
		c.Logger.WarnF(c.Ctx, fmt.Sprintf("client [%s] disconnect err: %s", c.Id, err))
	}
	c.AudioCh <- []byte("AUDIO::PANIC")
	time.Sleep(100 * time.Millisecond)
	//关闭后直接注销客户端
	WsManager.UnRegisterClient(c)

	c.Logger.InfoF(c.Ctx, fmt.Sprintf("client [%s],客户端关闭：[%d]", c.Id, websocket.CloseMessage))
}

func (c *Client) InitCriteria() error {
	if c.Bus.Criteria == nil {
		// var dr []tables.DResult
		var dc []tables.Career
		c.Bus.Criteria = make(map[int][]string)

		// 先获取职业的能力
		whereStr := ""
		switch c.Bus.Session.CareerID {
		case tables.QCivilServant:
			whereStr = "id = ?"
		case tables.BankTrainee:
			whereStr = fmt.Sprintf("id in (?,%d)", tables.SelfIntroduction) // 14-公共能力
		default:
			whereStr = fmt.Sprintf("id in (?,%d,%d)", tables.QCommonId, tables.SelfIntroduction)
		}
		d := variable.DB.MySQL.Model(&tables.Career{}).Where(whereStr, c.Bus.Session.CareerID).Order("id desc").Find(&dc)
		if d.Error != nil {
			return errors.New("该题库没有考察维度")
		}
		for i := 0; i < len(dc); i++ {
			var ts []string
			s := dc[i].SelectRule
			json.Unmarshal([]byte(s), &ts)
			c.Bus.Criteria[int(dc[i].ID)] = append(c.Bus.Criteria[int(dc[i].ID)], ts...)
		}
		// if c.Bus.Session.CareerID != 3 {
		// err := variable.DB.MySQL.Model(&tables.Question{}).
		// 	Where("career_id in (1, ?)", c.Bus.Session.CareerID).
		// 	Distinct("career_id", "criteria").
		// 	Scan(&dr).Error
		// fmt.Println(err)
		// } else {
		// 	if err := variable.DB.MySQL.Model(&tables.Question{}).
		// 		Where("career_id=?", c.Bus.Session.CareerID).
		// 		Distinct("career_id", "criteria").
		// 		Scan(&dr).Error; err != nil {
		// 		return err
		// 	}
		// }

		// for i := 0; i < len(dr); i++ {
		// 	c.Bus.Criteria[int(dr[i].CareerId)] = append(c.Bus.Criteria[int(dr[i].CareerId)], dr[i].Criteria)
		// }
	}
	if len(c.Bus.Criteria) == 0 {
		return errors.New("该题库没有考察维度")
	}
	return nil
}

func (c *Client) RecoverGoroutineCtx() {
	if res := recover(); res != nil {
		if fmt.Sprint(res) != "send on closed channel" {
			c.Logger.ErrorMsgF(c.Ctx, fmt.Sprintf("异常：%s", fmt.Sprint(res)))
		}
		const size = 64 << 10
		buf := make([]byte, size)
		buf = buf[:runtime.Stack(buf, false)]
		c.Logger.ErrorF(c.Ctx,
			fmt.Sprintf("[协程%d]", tools.GetGid()),
			"异常恢复, ",
			"异常内容: ", fmt.Sprint(res),
			" trace: ", string(buf),
		)
	}
}

func (c *Client) GetQuestionsByCareerId(careerId int) ([]*tables.Question, error) {
	questions := make([]*tables.Question, 100)
	mod := len(c.Bus.Criteria[careerId])
	if len(c.Bus.Criteria[int(c.Bus.Session.CareerID)]) > len(c.Bus.Criteria[careerId]) {
		mod = len(c.Bus.Criteria[int(c.Bus.Session.CareerID)])
	}
	tag := c.Bus.CurrentTag
	if careerId != tables.SelfIntroduction && careerId != tables.QCivilServant {
		tag = c.Bus.CurrentTag - 1
	}
	if err := variable.DB.MySQL.
		Where("type!=? and career_id=? and criteria=?", "extra_question", // 不是追问
			careerId, c.Bus.Criteria[careerId][tag%mod]).
		Order("id").Find(&questions).Error; err != nil {
		return nil, err
	}
	if len(questions) <= 0 {
		return nil, fmt.Errorf("[%s]这个维度没有标准问", c.Bus.Criteria[careerId][c.Bus.CurrentTag])
	}
	return questions, nil
}

func (c *Client) GetQuestions() ([]*tables.Question, error) {
	questions := make([]*tables.Question, 100)
	tag := c.Bus.CurrentTag - 1
	if c.Bus.Session.CareerID == 2 {
		tag = c.Bus.CurrentTag
	}
	if err := variable.DB.MySQL.
		Where("type!=? and career_id=? and criteria=?", "extra_question", // 不是追问–
			c.Bus.Session.CareerID, c.Bus.Criteria[int(c.Bus.Session.CareerID)][tag%len(c.Bus.Criteria[int(c.Bus.Session.CareerID)])]).
		Order("id").Find(&questions).Error; err != nil {
		return nil, err
	}
	if len(questions) <= 0 {
		return nil, fmt.Errorf("[%s]这个维度没有标准问", c.Bus.Criteria[int(c.Bus.Session.CareerID)][c.Bus.CurrentTag%len(c.Bus.Criteria[int(c.Bus.Session.CareerID)])])
	}
	return questions, nil
}

// 通用追问
func (c *Client) GetNoTagExtraQuestion() (*tables.Question, error) {
	// 确定选择的问题
	questions := make([]*tables.Question, 100)
	if len(c.Bus.PointStruct) == 0 || (len(c.Bus.PointStruct) != 0 && c.Bus.PointStruct[0] == 0) {
		if err := variable.DB.MySQL.
			Where("parent_text=? and career_id=?", c.Bus.CurrentQuestion.Text, c.Bus.CurrentQuestion.CareerID).
			Order("is_order asc").Find(&questions).Error; err != nil {
			return nil, err
		}
	} else {
		if err := variable.DB.MySQL.
			Where("parent_text=? and career_id=? and point_type in (?)", c.Bus.CurrentQuestion.Text, c.Bus.CurrentQuestion.CareerID, c.Bus.PointStruct).
			Order("is_order asc").Find(&questions).Error; err != nil {
			return nil, err
		}
	}

	if len(questions) == 0 {
		return nil, errors.New("没有相应的追问题目")
	}

	// quesSub := rand.Intn(len(questions))
	quesSub := 0
	return questions[quesSub], nil
}

// 猎聘追问
func (c *Client) GetExtraQuestion(keys []string) (*tables.Question, error) {
	questions := make([]*tables.Question, 100)
	if err := variable.DB.MySQL.
		Where("parent_text=? and career_id=? and tag in (?)", c.Bus.CurrentQuestion.Text, c.Bus.CurrentQuestion.CareerID, keys).
		Order("id").Find(&questions).Error; err != nil {
		return nil, err
	}
	if len(questions) == 0 {
		return nil, errors.New("没有相应的追问题目")
	}
	quesSub := rand.Intn(len(questions))
	return questions[quesSub], nil
}

func (c *Client) GetFinishVideo() (*tables.MarginVideo, error) {
	finishVideo := make([]*tables.MarginVideo, 10)
	if err := variable.DB.MySQL.
		Where("type=?", "finish").Find(&finishVideo).Error; err != nil {
		return nil, err
	}
	if len(finishVideo) <= 0 {
		return nil, errors.New("无可用的结束视频")
	}
	finishSub := rand.Intn(len(finishVideo))
	update := map[string]interface{}{
		"status":    c.getFinishedStatus(),
		"finish_at": time.Now(),
		"duration":  time.Now().Unix() - c.Bus.Session.TrainAt.Unix(),
	}
	variable.DB.MySQL.Model(&tables.Session{}).Where("id = ?", c.Bus.Session.ID).Updates(update)
	return finishVideo[finishSub], nil
}

// 专项练习面试完成
func (c *Client) specialFinish() error {
	// 1. 需要上传PCM文件
	dateTime := time.Now().Format("2006-01-02")
	fileName := fmt.Sprintf("/static/video/%s/%d_%d_%d_%s.pcm", dateTime, c.Bus.SpecialRecord.UserID, c.Bus.SpecialRecord.ID, c.Bus.SpecialRecord.CareerID, c.Bus.SpecialRecord.Type)
	if err := sdk.IOput(c.AudioBuffer.Bytes(), variable.Config.UfsInfo.BucketPrefix+fileName, "application/octet-stream"); err != nil {
		c.Logger.ErrorF(c.Ctx, fmt.Sprintf("音频文件上传失败：%s", fileName))
		return err
	}
	// 2.将音频url写入数据库中 更新失败了也没事
	if d := variable.DB.MySQL.Model(&tables.SpecialRecord{}).Where("id = ?", c.Bus.SpecialRecord.ID).
		Update("answer_audio", fileName); d.Error != nil {
		c.Logger.ErrorF(c.Ctx, fmt.Sprintf("音频文件上传失败：%s", fileName))
		return d.Error
	} else {
		c.Logger.InfoF(c.Ctx, fmt.Sprintf("音频文件上传成功：%s", fileName))
	}

	// 3. 将音频转mp3，出现错误也不能影响面试
	go func() {
		filePrefix := variable.Config.UfsInfo.UfsUrl + variable.Config.UfsInfo.BucketPrefix
		mp3Name, err := online_service.OptionFfmpegToMp3(filePrefix+fileName, int(c.Bus.SpecialRecord.ID))
		if err != nil {
			c.Logger.InfoMsgF(c.Ctx, fmt.Sprintf("音频转MP3文件上传失败：%s", mp3Name))
		}
		audioUrl := fmt.Sprintf(filePrefix+"/static/mp3/%s/%s", dateTime, mp3Name)
		d := variable.DB.MySQL.Model(&tables.SpecialRecord{}).Where("id = ?", c.Bus.SpecialRecord.ID).Update("audio", audioUrl)
		if d.Error != nil {
			c.Logger.InfoMsgF(c.Ctx, fmt.Sprintf("audio 更新error：%s", mp3Name))
		}
		// 将信息缓存出来，可以异步循环调用接口
		bc := variable.DB.Redis.HSet(fmt.Sprintf(common.RedisSpecialInfoKey, c.Bus.SpecialRecord.ID), common.RedisHmapAudio, audioUrl)
		if bc.Err() != nil {
			c.Logger.ErrorMsgF(c.Ctx, fmt.Sprintf("audio redis更新失败%s", mp3Name))
		}
		bc = variable.DB.Redis.HSet(fmt.Sprintf(common.RedisSpecialInfoKey, c.Bus.SpecialRecord.ID), common.RedisHmapTrans, tables.TranslateDoneStatus)
		if bc.Err() != nil {
			c.Logger.ErrorMsgF(c.Ctx, "redis 更新trans状态error")
		}
	}()

	return nil
}

func (c *Client) GetCurRealQues() (*tables.RealQues, error) {
	c.Bus.CurrentRealQues = new(tables.RealQues)
	if err := variable.DB.MySQL.
		Where("session_id=? and question_id=?", c.Bus.Session.ID, c.Bus.CurrentQuestion.ID).Preload("Question").
		First(c.Bus.CurrentRealQues).Error; err != nil {
		c.Logger.ErrorF(c.Ctx, "查询客户端对应的当前问题记录错误: "+err.Error())
		return nil, err
	}
	return c.Bus.CurrentRealQues, nil
}

func (c *Client) GetConvVideo(answer string) []byte {
	speakTime := c.Bus.SpeakTime
	realQues, err := c.GetCurRealQues()
	if err != nil {
		return c.ResClient("error", &VideoInfo{}, err.Error())
	}
	if realQues.Question.Criteria == "自我介绍" {
		return c.GetSelfIntroductionConv()
	}
	// 含有追问
	c.Logger.InfoF(c.Ctx, fmt.Sprintf("问题类型:%s , 缺少id: %x, 回答时长：%d", c.Bus.CurrentQuestion.Type, c.Bus.PointStruct, speakTime))
	// a) 未达到题目个数，并且该题是追问，则需要转场
	if c.Bus.CurrentTag < c.getTotal() && (c.Bus.CurrentQuestion.Type == tables.QExtra || c.Bus.CurrentQuestion.Type == tables.QNo) {
		return c.GetConvSession(realQues, answer)
	}
	// b) 未达到题目个数，并且该题是主题干，判断是否需要转场
	if c.Bus.CurrentTag < c.getTotal() {
		if len(c.Bus.PointStruct) == 0 { // 1. 题目评分是0 则不追问 进行 转场
			return c.GetConvSession(realQues, answer)
		}
		if c.Bus.PointStruct[0] == 0 && c.Bus.SpeakTime > 60 { // 2. 要点全覆盖 & 回答时长大于1min 则不追问 进行转场
			return c.GetConvSession(realQues, answer)
		}
		//b := c.NextQuestion(0)
		//if c.Bus.CurrentTag <= c.getTotal() { // 最后一道题，反而下一题了，那么就需要结束
		//	return b
		//}
	}
	// 需要调用转场
	if c.Bus.CurrentTag < c.getTotal() {
		return c.GetConvSession(realQues, answer)
	} else {
		// 面试过程 判断  面试题的个数
		// 社招面试是6道题  公务员面试是4道题  通用职业面试是4道题，索引不能通过

		// 面试完成后需要更新面试次数，未完成的面试不记录面试次数
		u, err := tables.GetUserInfoById(int(c.Bus.Session.UserID))
		if err != nil {
			return c.ResClient("error", &VideoInfo{}, err.Error())
		}
		where := make(map[string]interface{}, 0)
		where["train_count"] = u.TrainCount + 1
		//if u.Type == tables.UserTypeAnonymous && u.TrailCount > 0 {
		//	where["trail_count"] = u.TrailCount - 1
		//}
		tables.UpdateMapUserInfoById(where, int(u.ID)) // 更新面试次数
		finishVideo, err := c.GetFinishVideo()
		if err != nil {
			return c.ResClient("error", &VideoInfo{}, err.Error())
		}
		return c.ResClient("finish", &VideoInfo{
			Video:   finishVideo.Video,
			PcVideo: finishVideo.PcVideo,
			Text:    finishVideo.Text,
		})
	}
}

func (c *Client) ResClient(code string, videoInfo *VideoInfo, msg ...string) []byte {
	var qo []string
	err := json.Unmarshal([]byte(videoInfo.QuestionOrigin), &qo)
	if err != nil {
		qo = make([]string, 0)
	}
	res := &VideoRes{
		Type: code,
		Data: struct {
			Video          string   `json:"video"`
			PcVideo        string   `json:"pc_video"`
			Text           string   `json:"text"`
			Current        int      `json:"current"`
			OldText        string   `json:"old_text"`
			Total          int      `json:"total"`
			QuestionOrigin []string `json:"question_origin"`
		}{
			Video:          videoInfo.Video,
			PcVideo:        videoInfo.PcVideo,
			Text:           videoInfo.Text,
			Current:        c.Bus.CurrentTag,
			Total:          c.getTotal(),
			QuestionOrigin: qo,
		},
		Message: msg,
	}
	if code == "extra" {
		res.Data.OldText = c.Bus.CurrentQuestion.ParentText
	}
	serialData, _ := json.Marshal(res)
	//c.Logger.InfoF(c.Ctx, ""+string(serialData))
	return serialData
}

func (c *Client) CreateRealQues(BelongToID *uint) []byte {
	var questionType = 0
	if c.Bus.CurrentQuestion.Type == tables.QExtra {
		questionType = 1
	}
	realQues := tables.RealQues{
		QuestionID:     c.Bus.CurrentQuestion.ID,
		SessionID:      c.Bus.Session.ID,
		QuestionType:   uint(questionType),
		QuestionText:   c.Bus.CurrentQuestion.Text,
		CurrentTag:     uint(c.Bus.CurrentTag),
		BelongToID:     BelongToID,
		QuestionOrigin: c.Bus.CurrentQuestion.QuestionOrigin,
		AnsAnalysisTag: c.Bus.CurrentQuestion.AnsAnalysisTag,
	}

	if err := variable.DB.MySQL.Select("QuestionID", "SessionID", "QuestionType", "QuestionText", "CurrentTag", "BelongToID", "QuestionOrigin", "AnsAnalysisTag").
		Create(&realQues).Error; err != nil {
		c.Logger.ErrorF(c.Ctx, "客户端对应的当前问题记录入库失败")
		return c.ResClient("error", &VideoInfo{}, "客户端对应的当前问题记录入库失败:"+err.Error())
	}
	c.Bus.CurrentRealQues = &realQues
	return nil
}

func (c *Client) GetConvSession(realQues *tables.RealQues, answer string) []byte {
	marginVideo, err := ConvSession(realQues, answer)
	if err != nil {
		return c.ResClient("error", &VideoInfo{}, err.Error())
	}
	return c.ResClient("conv", &VideoInfo{
		Video:   marginVideo.Video,
		PcVideo: marginVideo.PcVideo,
		Text:    marginVideo.Text,
	})
}

func (c *Client) GetSelfIntroductionConv() []byte {
	marginVideos := make([]*tables.MarginVideo, 10)
	if err := variable.DB.MySQL.
		Where("tag=? and type=?", "无", "convert").
		Find(&marginVideos).Error; err != nil {
		return c.ResClient("error", &VideoInfo{}, err.Error())
	}
	if len(marginVideos) <= 0 {
		return c.ResClient("error", &VideoInfo{}, "无可用转场视频")
	}
	randSub := rand.Intn(len(marginVideos))
	convVideo := marginVideos[randSub]
	return c.ResClient("conv", &VideoInfo{
		Video:   convVideo.Video,
		PcVideo: convVideo.PcVideo,
		Text:    convVideo.Text,
	})
}

// 从websocket中直接读取数据
func (c *Client) Read() {
	defer func() { c.Close() }()
	for {
		messageType, message, err := c.Socket.ReadMessage()
		//读取数据失败
		if err != nil {
			c.Logger.InfoF(c.Ctx, fmt.Sprintf("client [%s],messageType: %d, message: %s, err: %v", c.Id, messageType, string(message), err))
			if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
				if messageType == -1 || len(string(message)) == 0 {
					break
				}
				c.Logger.WarnMsgF(c.Ctx, fmt.Sprintf("client [%s],messageType: %d, message: %s, 数据读取失败或通道关闭：[%v]", c.Id, messageType, string(message), err))
			}
			if messageType == websocket.CloseMessage {
				c.Logger.ErrorMsgF(c.Ctx, fmt.Sprintf("client [%s],数据读取失败或通道关闭：[%v]", c.Id, err))
			}
			break
		}
		var newMsg = string(message)
		if strings.Contains(newMsg, "AUDIO::FINISH") {
			sarr := strings.Split(newMsg, "::")
			speakTime := sarr[len(sarr)-2]
			currentTag := sarr[len(sarr)-1]
			newMsg = strings.Replace(newMsg, "::"+speakTime+"::"+currentTag, "", -1)
			st, _ := strconv.Atoi(speakTime)
			c.Bus.SpeakTime = st
			ct, _ := strconv.Atoi(currentTag)
			c.Bus.CurrentTagOld = ct // 记录前端返回的当前题目值
			c.Logger.InfoF(c.Ctx, fmt.Sprintf("speakTime: %d", st))
		} else if strings.Contains(newMsg, "AUDIO::SPECIAL::FINISH") {
			sarr := strings.Split(newMsg, "::")
			speakTime := sarr[len(sarr)-1]
			newMsg = strings.Replace(newMsg, "::"+speakTime, "", -1)
			st, _ := strconv.Atoi(speakTime)
			c.Bus.SpeakTime = st
			c.Logger.InfoF(c.Ctx, fmt.Sprintf("speakTime: %d", st))
		}
		switch newMsg {
		case "ping":
			c.Message <- c.ResClient("pong", &VideoInfo{})

		case "AUDIO::INIT":
			c.Logger.InfoF(c.Ctx, "msg:AUDIO::INIT")
			variable.DB.MySQL.Unscoped().Delete(&tables.RealQues{}, &tables.RealQues{SessionID: c.Bus.Session.ID})
			c.Bus.CurrentTag = 0
			c.Bus.Classify = MainClassify // 设置练习类型
			c.AudioBuffer = new(bytes.Buffer)
			err := c.InitCriteria() // 判断该职业是否有题库存在，兼容通用岗位题库
			if err != nil {
				c.Message <- c.ResClient("error", &VideoInfo{}, err.Error())
				continue
			}
			c.Logger.InfoF(c.Ctx, fmt.Sprintf("第[%d]道面试题", c.Bus.CurrentTag+1))
			c.Message <- c.FirstQuesVideo() //第一道面试题

		case "AUDIO::BEGIN":
			c.Logger.Info("准备连接讯飞websocket, msg:AUDIO::BEGIN")
			go func() {
				defer c.RecoverGoroutineCtx()
				ConnServer(c.Ctx, c.AudioCh, c.TextCh)
			}()
			c.Bus.CombineText = ""

		case "OCCUPY": // 前端发送
			c.Logger.Info("msg:OCCUPY")
			// 将信息存储起来
			b, _ := json.Marshal(c.Bus)
			variable.DB.Redis.Set(fmt.Sprintf(common.RedisRetryConn, c.Bus.Session.ID), string(b), 600*time.Second)
			WsManager.UnRegisterClient(c) // 关闭客户端
			c.AudioCh <- []byte("AUDIO::FINISH")

		case "REJOIN": // 前端发送
			c.AudioBuffer = new(bytes.Buffer)
			sc := variable.DB.Redis.Get(fmt.Sprintf(common.RedisRetryConn, c.Bus.Session.ID))
			scResult, _ := sc.Result()
			if len(scResult) != 0 {
				var bus = Business{}
				err := json.Unmarshal([]byte(scResult), &bus)
				if err != nil {
					c.Message <- c.ResClient("rejoin_error", &VideoInfo{}, "重连失败")
				} else {
					bus.CombineText = "" // 重新初始化数据
					c.Bus = bus
					variable.DB.Redis.Del(fmt.Sprintf(common.RedisRetryConn, c.Bus.Session.ID)) // 删除key
					c.Logger.InfoF(c.Ctx, fmt.Sprintf("websocket retry:%d, websocket client info: %+v, interview special_record info:%s", c.Bus.Session.ID, c, string(scResult)))
					c.Message <- c.ResClient("rejoin_finish", &VideoInfo{}, "重连成功")
				}
			} else {
				c.Message <- c.ResClient("rejoin_error", &VideoInfo{}, "重连失败")
			}
		case "AUDIO::CONV::FINISH":
			c.Logger.InfoF(c.Ctx, fmt.Sprintf("msg:AUDIO::CONV::FINISH, 第[%d]道面试题", c.Bus.CurrentTag))
			c.Message <- c.NextQuestion(1)
		case "AUDIO::FINISH":
			c.Logger.InfoF(c.Ctx, "msg: AUDIO::FINISH")
			rq, _ := c.GetCurRealQues()
			c.Bus.CurrentId = rq.ID
			c.AudioCh <- []byte(newMsg) // 讯飞强制断开
			if c.AudioBuffer.Len() == 0 {
				c.TextCh <- "EOFS"
			}
			dateTime := time.Now().Format("2006-01-02")
			fileName := fmt.Sprintf("/static/video/%s/%d_%d_%d_%s.pcm", dateTime, c.Bus.Session.ID, c.Bus.CurrentQuestion.ID, c.Bus.CurrentTag, c.Bus.CurrentQuestion.Type)
			if err := sdk.IOput(c.AudioBuffer.Bytes(), variable.Config.UfsInfo.BucketPrefix+fileName, "application/octet-stream"); err != nil {
				c.Logger.ErrorMsgF(c.Ctx, fmt.Sprintf("pcm upload is error, id: %d, pcm path: %s", rq.ID, fileName))
			}
			c.AudioBuffer.Reset()
			// 将音频url写入数据库中 更新失败了也没事
			if d := variable.DB.MySQL.Model(&tables.RealQues{}).Where("id = ?", rq.ID).Updates(map[string]interface{}{"answer_audio": fileName, "current_tag": c.Bus.CurrentTagOld}); d.Error != nil {
				c.Logger.ErrorMsgF(c.Ctx, fmt.Sprintf("pcm update is error, id %d, pcm path: %s", rq.ID, fileName))
			} else {
				c.Logger.InfoF(c.Ctx, fmt.Sprintf("pcm update is success, id %d, pcm path: %s", rq.ID, fileName))
			}
			// 将音频转mp3，出现错误也不能影响面试
			go func() {
				c.Logger.InfoF(c.Ctx, fmt.Sprintf("career_id:%d, current_tag:%d,", c.Bus.Session.CareerID, c.Bus.CurrentTagOld))
				// if c.Bus.Session.CareerID != 2 && c.Bus.CurrentTagOld == 1 { // 1. 公务员没有自我介绍；2. 除公务员外，其他面试只有第一道题自我介绍才进行屏蔽敏感词；
				// 	fn, err := new(report_service.MaskService).MaskWords(c.Ctx, int(c.Bus.CurrentId))
				// 	if err != nil {
				// 		c.Logger.InfoMsgF(c.Ctx, fmt.Sprintf("id:%d, err:%s", c.Bus.CurrentId, err.Error()))
				// 	}
				// 	if len(fn) != 0 { // 兼容无需上传的情况
				// 		fileName = fn
				// 	}
				// }
				filePrefix := variable.Config.UfsInfo.UfsUrl + variable.Config.UfsInfo.BucketPrefix
				mp3Name, err := online_service.OptionFfmpegToMp3(filePrefix+fileName, int(rq.ID))
				if err != nil {
					c.Logger.InfoMsgF(c.Ctx, fmt.Sprintf("mp3 upload is error, id: %d, mp3 path: %s", rq.ID, mp3Name))
				}
				mp3Path := fmt.Sprintf(filePrefix+"/static/mp3/%s/%s", dateTime, mp3Name)
				d := variable.DB.MySQL.Model(&tables.RealQues{}).Where("id = ?", rq.ID).Update("audio", mp3Path)
				if d.Error != nil {
					c.Logger.InfoMsgF(c.Ctx, fmt.Sprintf("mp3 update error, id: %d, path:%s", rq.ID, mp3Path))
				}
			}()
		case "AUDIO::SPECIAL::BEGIN":
			c.Logger.InfoF(c.Ctx, fmt.Sprintf("speakTime[%d], newMsg: %s", c.Bus.SpeakTime, newMsg))
			// 1. 设置面试类型
			c.Bus.Classify = SpecialClassify
			c.AudioBuffer = new(bytes.Buffer)
			// 2. 连接讯飞asr接口
			c.Logger.Debug("准备连接讯飞websocket")
			go func() {
				defer c.RecoverGoroutineCtx()
				ConnServer(c.Ctx, c.AudioCh, c.TextCh)
			}()
			c.Bus.CombineText = ""
			// 3. 更新面试为面试中
			var updateMap map[string]interface{}
			updateMap = make(map[string]interface{}, 0)
			updateMap["status"] = tables.IoStatus
			updateMap["trans_status"] = tables.TranslateDoneStatus
			updateMap["train_at"] = time.Now()
			updateMap["total_score"] = -1 // 初始化分数为 -1
			err := new(career_service.SpecialService).UpdateSpecial(c.Ctx, c.Bus.SpecialRecord.ID, updateMap)
			if err != nil {
				return
			}
			// 初始化一下redis
			variable.DB.Redis.HSet(fmt.Sprintf(common.RedisSpecialInfoKey, c.Bus.SpecialRecord.ID), common.RedisHmapScore, -1)

		case "AUDIO::SPECIAL::FINISH":
			c.Logger.InfoF(c.Ctx, fmt.Sprintf("speakTime[%d], newMsg: %s", c.Bus.SpeakTime, newMsg))
			// 1. 讯飞强制断开
			c.AudioCh <- []byte(newMsg)

			// 2.写入音频到文件
			err := c.specialFinish()
			if err != nil {
				c.Logger.ErrorMsgF(c.Ctx, fmt.Sprintf("音频写入失败， err info: %s", err.Error()))
				return
			}

			// 3. 释放缓存
			c.AudioBuffer.Reset()

			// 4. 更新面试状态
			var updateMap map[string]interface{}
			updateMap = make(map[string]interface{}, 0)
			updateMap["status"] = tables.ProStatus
			updateMap["finish_at"] = time.Now()
			err = new(career_service.SpecialService).UpdateSpecial(c.Ctx, c.Bus.SpecialRecord.ID, updateMap)
			if err != nil {
				c.Logger.ErrorMsgF(c.Ctx, fmt.Sprintf("面试状态更新失败， err info: %s", err.Error()))
				return
			}
			// 5. 将用户面试过的问题写入到redis 用于过滤
			redisKey := fmt.Sprintf(common.RedisSpecialTopicKey, c.Bus.SpecialRecord.UserID, c.Bus.SpecialRecord.CareerID, c.Bus.SpecialRecord.Criteria, c.Bus.SpecialRecord.Level)
			sc := variable.DB.Redis.Get(redisKey)
			sr, _ := sc.Result()
			var arr []uint
			if len(sr) != 0 {
				json.Unmarshal([]byte(sr), &arr)
			}
			arr = append(arr, c.Bus.SpecialRecord.QuestionID)
			b, _ := json.Marshal(arr)
			sc2 := variable.DB.Redis.Set(redisKey, string(b), -1)
			_, err = sc2.Result()
			if err != nil {
				c.Logger.ErrorMsgF(c.Ctx, fmt.Sprintf("设置redis err info: %s", err.Error()))
				return
			}

			// 6. 异步计算得分
			go func() {
				err = new(career_service.SpecialService).UpdateScore(c.Ctx, int(c.Bus.SpecialRecord.ID))
				if err != nil {
					c.Logger.ErrorMsgF(c.Ctx, fmt.Sprintf("计算得分 err info: %s", err.Error()))
					return
				}
				// 7. 将数据添加入监控  错误忽略
				variable.DB.Redis.RPush(common.RedisSpecialQueue, c.Bus.SpecialRecord.ID)
			}()
			c.Message <- c.ResClient("special_finish", &VideoInfo{}, "消息接收完成:ok")

		default:
			if len(string(message)) != 0 && c.AudioBuffer != nil {
				c.AudioBuffer.Write(message)
				c.AudioCh <- message
			}
		}
	}
}

func (c *Client) NextQuesReqData() ([]byte, error) {
	realQues, err := c.GetCurRealQues()
	if err != nil {
		return nil, err
	}
	nextQuesReq := &NextQuesReq{
		Name: "next_question",
		Query: []struct {
			QuestionID uint   `json:"c_question_id"`
			Question   string `json:"question"`
			Answer     string `json:"answer"`
		}{{
			QuestionID: c.Bus.CurrentQuestion.ID,
			Question:   c.Bus.CurrentQuestion.Text,
			Answer:     realQues.AnswerText,
		}},
	}
	nextQuesReqData, _ := json.Marshal(nextQuesReq)
	c.Logger.InfoF(c.Ctx, "请求追问服务的数据: "+string(nextQuesReqData))
	return nextQuesReqData, nil
}

// 下一通用问题
func (c *Client) GetNextCommonQuestion() []byte {
	nextQues, err := c.GetQuestionsByCareerId(tables.QCommonId) // 下一个通用问题
	if err != nil {
		return c.ResClient("error", &VideoInfo{}, err.Error())
	}
	quesSub := rand.Intn(len(nextQues))
	c.Bus.CurrentQuestion = nextQues[quesSub]
	c.Bus.CurrentTag += 1
	if ok := c.CreateRealQues(nil); ok != nil {
		return ok
	}
	return c.ResClient("ask", &VideoInfo{
		Video:          c.Bus.CurrentQuestion.Video,
		PcVideo:        c.Bus.CurrentQuestion.PcVideo,
		Text:           c.Bus.CurrentQuestion.Text,
		QuestionOrigin: c.Bus.CurrentQuestion.QuestionOrigin,
	})
}

func (c *Client) GetNextNormalQuestion() []byte {
	var nextQues []*tables.Question
	// 如果问题小于当前职业能力的个数，否则走通用题库
	var err error
	if c.Bus.CurrentTag-1 < len(c.Bus.Criteria[int(c.Bus.Session.CareerID)]) {
		nextQues, err = c.GetQuestions()
	} else {
		nextQues, err = c.GetQuestionsByCareerId(tables.QCommonId)
	}
	if err != nil {
		return c.ResClient("error", &VideoInfo{}, err.Error())
	}
	quesSub := rand.Intn(len(nextQues))
	c.Bus.CurrentQuestion = nextQues[quesSub]
	c.Bus.CurrentTag += 1
	if ok := c.CreateRealQues(nil); ok != nil {
		return ok
	}
	return c.ResClient("ask", &VideoInfo{
		Video:          c.Bus.CurrentQuestion.Video,
		PcVideo:        c.Bus.CurrentQuestion.PcVideo,
		Text:           c.Bus.CurrentQuestion.Text,
		QuestionOrigin: c.Bus.CurrentQuestion.QuestionOrigin,
	})
}

// 写入数据到websocket中
func (c *Client) Write() {
	defer func() { c.Close() }()
	for {
		select {
		case message, ok := <-c.Message:
			if !ok {
				//数据写入失败，关闭通道
				c.Logger.WarnF(c.Ctx, fmt.Sprintf("client [%s],客户端连接状态：[%d]", c.Id, websocket.CloseMessage))
				_ = c.Socket.WriteMessage(websocket.CloseMessage, []byte{})
				//消息通道关闭后直接注销客户端
				return
			}
			err := c.Socket.WriteMessage(websocket.TextMessage, message)
			if err != nil {
				c.Logger.WarnF(c.Ctx, fmt.Sprintf("client [%s] write message err: %s", c.Id, err))
			} else {
				c.Logger.DebugF(c.Ctx, "发送给前端的ws数据: "+string(message))
			}

		case audioText, ok := <-c.TextCh:
			if !ok {
				c.Logger.ErrorF(c.Ctx, "文本接收通道错误")
			} else {
				if audioText == "EOFS" {
					var answerText = c.Bus.CombineText
					var lackPointType = make([]int, 0)
					c.Logger.InfoF(c.Ctx, "收到文本结果: "+answerText)
					update := make(map[string]interface{})
					update["answer_text"] = answerText
					update["speak_time"] = uint(c.Bus.SpeakTime)
					// 需要进行要点评分
					if c.Bus.Classify == SpecialClassify || c.Bus.Session.Career.ReportType == "pdf" {
						pointText := c.Bus.CurrentQuestion.PointText
						c.Logger.InfoF(c.Ctx, "pointText:"+pointText+"combineText:"+answerText)
						if len(pointText) != 0 && len(answerText) > 50 { // 必须有文本才能进行评分
							var pointScore = 0.00
							var pts []tables.PointTextStruct // 要点评分数据结构
							var pointScoreDetail []tables.PointTextDetail
							_ = json.Unmarshal([]byte(pointText), &pts)
							for i := 0; i < len(pts); i++ {
								score := report_service.FetchQScore(answerText, pts[i].Value)
								pointScoreDetail = append(pointScoreDetail, tables.PointTextDetail{
									Type:  pts[i].Type,
									Label: pts[i].Label,
									Value: pts[i].Value,
									Score: score,
								})
								if score < 0.5 { // 缺少要点
									lackPointType = append(lackPointType, i+1)
								}
								pointScore += score
							}
							if len(lackPointType) == 0 { // 说明不缺少要点
								lackPointType = append(lackPointType, 0)
							}
							avgPointScore := pointScore / float64(len(pts))
							b, _ := json.Marshal(pointScoreDetail)
							update["main_point_score_detail"] = string(b)
							update["main_point_score"] = avgPointScore
							c.Logger.InfoF(c.Ctx, "pointText: "+string(b))
						}
					}
					// 主流程
					if c.Bus.Classify == MainClassify {
						d := variable.DB.MySQL.Model(&tables.RealQues{}).Where("id = ?", c.Bus.CurrentId).Updates(update)
						if d.Error != nil {
							c.Logger.ErrorMsgF(c.Ctx, "answer_text update error")
						}
						c.Bus.PointStruct = lackPointType
						video := c.GetConvVideo(answerText)
						c.Message <- video
						// 对每道题的回答文本进行算法标签词提取，并缓存起来，作为在最后标签词评测的请求参数
						if c.Bus.Session.Career.ReportType == "web" {
							go func() {
								defer c.RecoverGoroutineCtx()
								t1 := time.Now().UnixMilli()
								partLabelsData := report_service.FetchLabelsCache(c.Ctx, c.Bus.Session.Career.Name, answerText)
								if err := variable.DB.MySQL.Model(c.Bus.CurrentRealQues).
									Update("part_labels_cache", partLabelsData).Error; err != nil {
									c.Logger.ErrorF(c.Ctx, err.Error())
								}
								var partLabelsCache interface{}
								_ = json.Unmarshal([]byte(partLabelsData), &partLabelsCache)
								if c.partCacheLabels == nil {
									c.partCacheLabels = make([]interface{}, 0)
								}
								c.partCacheLabels = append(c.partCacheLabels, partLabelsCache)
								if err := c.CacheNextQues(*c); err != nil {
									c.Logger.ErrorF(c.Ctx, err.Error())
								}
								t2 := time.Now().UnixMilli()
								c.Logger.Debug(fmt.Sprintf("[recommend]标签提取及评测以及推荐下一题共使用[%d]ms", t2-t1))
							}()
						}

					} else { // 专项练习
						update["trans_status"] = tables.TranslateDoneStatus
						d := variable.DB.MySQL.Model(&tables.SpecialRecord{}).Where("id = ?", c.Bus.SpecialRecord.ID).Updates(update)
						if d.Error != nil {
							c.Logger.ErrorMsgF(c.Ctx, "更新回答内容error")
						}
						bc := variable.DB.Redis.HSet(fmt.Sprintf(common.RedisSpecialInfoKey, c.Bus.SpecialRecord.ID), common.RedisHmapAnswer, answerText)
						if bc.Err() != nil {
							c.Logger.ErrorMsgF(c.Ctx, "answer_text 更新状态error")
						}
					}

				} else {
					// 否则，需要解开数据
					var wt = WsText{}
					err := json.Unmarshal([]byte(audioText), &wt)
					if err != nil {
						audioText = ""
					} else {
						audioText = wt.Text
						// 将数据进行缓存
						if c.Bus.Classify == MainClassify {
							// 获取当前问题的id 以及 当前题目id
							currentId := c.Bus.CurrentRealQues.ID // 当前题目的id
							tag := c.Bus.CurrentTag               // 当前题目的编号
							// 将数据缓存起来，用来进行音频敏感语音处理
							if tag == 1 {
								// 写入redis
								for i := 0; i < len(wt.Wi); i++ {
									bg, _ := strconv.ParseFloat(wt.Wi[i].Bg, 64)
									wb := wt.Wi[i].Wb
									b, _ := json.Marshal(wt.Wi[i])
									c.Logger.InfoF(c.Ctx, fmt.Sprintf(common.RedisSetMaskRIdKey, currentId))
									variable.DB.Redis.ZAdd(fmt.Sprintf(common.RedisSetMaskRIdKey, currentId), redis.Z{Member: string(b), Score: bg + wb*10}) // 失败了也没有关系，就是音频敏感词没有去除
								}
							}
						}
					}
					c.Bus.CombineText += audioText
				}
			}
		}
	}
}

type DefaultClient struct {
	*Client
}

func (c *DefaultClient) CacheNextQues(Client) error { return nil }

// flag 是 1 无需判断追问
func (c *DefaultClient) NextQuestion(flag int) []byte {
	if flag == 1 {
		return c.GetNextNormalQuestion()
	}
	quesID := strconv.Itoa(int(c.Bus.CurrentQuestion.ID))
	// 如果当前问题是 通用追问， 不需要使用猎聘的追问列表
	if c.Bus.CurrentQuestion.Type == tables.QCommon {
		// 进行追问
		c.Bus.PointStruct = append(c.Bus.PointStruct, 0) // 补个0,兼容不缺少要点的情况
		next, err := c.GetNoTagExtraQuestion()
		if err != nil {
			c.Logger.ErrorF(c.Ctx, "获取追问问题异常:"+err.Error()+";开始下一个标准问")
			return c.GetNextNormalQuestion() //下一个标准问
		}
		belongRealQues, _ := c.GetCurRealQues()
		c.Bus.CurrentQuestion = next
		if ok := c.CreateRealQues(&belongRealQues.QuestionID); ok != nil {
			return ok
		}
		return c.ResClient("extra", &VideoInfo{
			Video:          next.Video,
			PcVideo:        next.PcVideo,
			Text:           next.Text,
			QuestionOrigin: next.QuestionOrigin,
		})
	} else if c.Bus.CurrentQuestion.Type == tables.QHunter { // 问题已经不存在猎聘追问，可以废弃
		nextReqData, err := c.NextQuesReqData()
		if err != nil {
			return c.ResClient("error", &VideoInfo{}, err.Error())
		}
		resp, err := http.Post(
			variable.Config.Services.NextQuestion,
			"application/json",
			bytes.NewReader(nextReqData),
		)
		if err != nil {
			c.Logger.ErrorF(c.Ctx, "追问接口调用失败: "+err.Error())
			return c.ResClient("error", &VideoInfo{}, "追问接口调用失败: "+err.Error())
		}
		res := make(map[string]map[string]QuesInfo)
		err = tools.ReadResponseBody(resp, &res)
		if err != nil {
			errMsg := "追问接口响应结果解析失败" + err.Error()
			c.Logger.ErrorF(c.Ctx, errMsg)
			return c.ResClient("error", &VideoInfo{}, errMsg)
		}
		nextType := res["data"][quesID].Type
		keys := res["data"][quesID].Key
		if nextType == "追问" && len(keys) > 0 {
			next, err := c.GetExtraQuestion(keys)
			if err != nil {
				c.Logger.ErrorF(c.Ctx, "获取追问问题异常:"+err.Error()+";开始下一个标准问")
				return c.GetNextNormalQuestion() //下一个标准问
			}
			belongRealQues, _ := c.GetCurRealQues()
			c.Bus.CurrentQuestion = next
			if ok := c.CreateRealQues(&belongRealQues.QuestionID); ok != nil {
				return ok
			}
			return c.ResClient("extra", &VideoInfo{
				Video:          next.Video,
				PcVideo:        next.PcVideo,
				Text:           next.Text,
				QuestionOrigin: next.QuestionOrigin,
			})
		}
	}
	return c.GetNextNormalQuestion()
}

func (c *DefaultClient) getTotal() int {
	totalQuesCnt := 0
	for _, criteria := range c.Bus.Criteria {
		totalQuesCnt += len(criteria)
	}
	return totalQuesCnt // 普通社招面试题数
}

func (c *DefaultClient) FirstQuesVideo() []byte {
	// 如果 不是公务员 第一道 取当前职业能力题库
	var careerId int
	if c.Bus.Session.CareerID == tables.QCivilServant {
		careerId = int(c.Bus.Session.CareerID)
	} else {
		careerId = tables.SelfIntroduction
	}
	questions, err := c.GetQuestionsByCareerId(careerId) // 14-是自我介绍的岗位ID
	if err != nil {
		return c.ResClient("error", &VideoInfo{}, err.Error())
	}
	quesSub := rand.Intn(len(questions))
	c.Bus.CurrentQuestion = questions[quesSub]
	if err1 := c.CreateRealQues(nil); err1 != nil {
		return err1
	}
	fmt.Println(c.Bus.CurrentQuestion)
	c.Bus.CurrentTag += 1
	return c.ResClient("ask", &VideoInfo{
		Video:          c.Bus.CurrentQuestion.Video,
		PcVideo:        c.Bus.CurrentQuestion.PcVideo,
		Text:           c.Bus.CurrentQuestion.Text,
		QuestionOrigin: c.Bus.CurrentQuestion.QuestionOrigin,
	})
}

func (c *DefaultClient) getFinishedStatus() int {
	return tables.IoStatus
}
